/* 
 * Copyright 2010 Ziya Guven Koylu, Ozge Koylu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.koylu.caffein.model.clazz;

import java.sql.ResultSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import javassist.util.proxy.ProxyFactory;
import javassist.util.proxy.ProxyObject;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.koylu.caffein.exception.ClazzConfigException;
import com.koylu.caffein.javassist.BeanGenerator;
import com.koylu.caffein.model.caffein.CaffeinConfig;
import com.koylu.caffein.model.clazz.id.Id;
import com.koylu.caffein.model.clazz.validation.ValidateFor;
import com.koylu.caffein.model.container.ClazzContainer;
import com.koylu.caffein.model.container.Container;
import com.koylu.caffein.model.handler.ClazzContainerHandler;
import com.koylu.caffein.query.GenericQuery;

public class Clazz implements Node {

	private static Log log = LogFactory.getLog(Clazz.class);
	private String name;
	private String type;
	private String table;
	private String schema;
	private boolean cacheable = Boolean.FALSE;
	private boolean readonly = Boolean.FALSE;
	private Class<?> virtualClass;
	private Class<?> realClass;
	private Class<?> proxyClass;
	private Id id;
	private OptimisticLockProperty optimisticLockProperty;
	private Map<String, Field> fieldMap;
	private Map<String, Field> nonIdFieldsMap;
	private Map<String, Relation> relationMap;
	private Map<String, QueryProperty> queryPropertyMap;
	private Map<String, Column> idColumnMap;
	private Map<String, Column> columnMap;
	private Map<String, Column> allColumnMap;
	private Map<String, Column> allPathColumnMap;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	
	public String getType() {
		return type;
	}
	
	public void setType(String type) {
		this.type = type;
	}
	
	private String tableWithSchema;
	public String getTable() {
		if(tableWithSchema == null){
			if( schema == null || "".equals(schema) ){
				tableWithSchema = table;
			}else{
				tableWithSchema = schema+"."+table;
			}
		}
		return tableWithSchema;
	}

	public void setTable(String table) {
		this.table = table;
	}
	
	public String getSchema() {
		return schema;
	}
	
	public void setSchema(String schema) {
		this.schema = schema;
	}
	
	public boolean isCacheable(CaffeinConfig caffeinConfig) {
		if(caffeinConfig.getCacheManager() == null){
			return false;
		}
		return cacheable;
	}
	
	public boolean isCacheable() {
		return cacheable;
	}
	
	public void setCacheable(boolean cacheable) {
		this.cacheable = cacheable;
	}
	
	public boolean isReadonly() {
		return readonly;
	}
	
	public void setReadonly(boolean readonly) {
		this.readonly = readonly;
	}
	
	public Node getParent() {
		return null;
	}

	public void setParent(Node parent) {}
	
	public Class<?> getProxyClass(CaffeinConfig caffeinConfig) throws Exception{
		if(proxyClass == null){
			ProxyFactory proxyFactory = new ProxyFactory();
			proxyFactory.setSuperclass(getRealClass(caffeinConfig));
			proxyFactory.setInterfaces(ClazzContainer.interfaces);
			proxyFactory.setFilter(Container.methodFilter);
			proxyClass = proxyFactory.createClass();
		}
		return proxyClass;
	}
	
	public Class<?> getRealClass(CaffeinConfig caffeinConfig) throws Exception{
		if(realClass == null){
			realClass = toVirtualClass(caffeinConfig);
		}
		return realClass;
	}
	
	public Class<?> toVirtualClass(CaffeinConfig caffeinConfig) throws Exception {
		if(virtualClass == null){
			if( type == null || "".equals(type)  ){
				BeanGenerator beanGenerator = new BeanGenerator(name);
				for (Field field : getChildren()) {
					beanGenerator.addProperty(field.getName(), field.toVirtualClass(caffeinConfig));
				}
				virtualClass = beanGenerator.createClass();
			}else{
				if(BeanGenerator.isClassExist(type)){
					if(queryPropertyMap != null || optimisticLockProperty != null){
						BeanGenerator beanGenerator = new BeanGenerator(type, getName());
						if(queryPropertyMap != null){
							log.info("queryProperty found for clazz:"+getName()+" adding new fields:"+queryPropertyMap.keySet()+" to the class");
							for (Iterator<String> iterator = queryPropertyMap.keySet().iterator(); iterator.hasNext();) {
								beanGenerator.addProperty(iterator.next(), Object.class);
							}						
						}
						if( optimisticLockProperty != null){
							log.info("optimisticLockProperty found for clazz:"+getName()+" adding new field:"+optimisticLockProperty.getName()+" to the class");
							beanGenerator.addProperty(optimisticLockProperty.getName(), optimisticLockProperty.getConverter(caffeinConfig).getFieldType());						
						}
						virtualClass = beanGenerator.createClass();	
					}else{
						log.info("class found in the classpath:"+type);
						virtualClass = Class.forName(type);	
					}
				}else{
					throw new ClazzConfigException("class not found:"+type, this);
				}				
			}
		}
		return virtualClass;
	}

	public Id getId() {
		return id;
	}

	public void setId(Id id) {
		this.id = id;
	}
	
	public OptimisticLockProperty getOptimisticLockProperty() {
		return optimisticLockProperty;
	}
	
	public void setOptimisticLockProperty(OptimisticLockProperty optimisticLockProperty) {
		this.optimisticLockProperty = optimisticLockProperty;
	}

	public void addChild(Node child) {
		if (fieldMap == null) {
			fieldMap = new TreeMap<String, Field>();
		}
		Field field = (Field) child;
		if(fieldMap.containsKey(field.getName())){
			log.warn("field["+field.getName()+"] already exist in the clazz-->"+name);
		}
		fieldMap.put(field.getName(), field);
		child.setParent(this);
		if (child instanceof Id) {
			if(getId() == null){
				setId((Id) child);
			}
		}else {
			if(nonIdFieldsMap == null){
				nonIdFieldsMap = new HashMap<String, Field>();
			}
			nonIdFieldsMap.put(field.getName(), field);
			if( child instanceof QueryProperty ){
				if(queryPropertyMap == null){
					queryPropertyMap = new HashMap<String, QueryProperty>();
				}
				queryPropertyMap.put(field.getName(), (QueryProperty)field);
			}else if (child instanceof Relation){
				if (relationMap == null) {
					relationMap = new TreeMap<String, Relation>();
				}
				relationMap.put(field.getName(), (Relation) field);
			}else if ( child instanceof OptimisticLockProperty ){
				if(getOptimisticLockProperty() == null){
					setOptimisticLockProperty((OptimisticLockProperty)field);					
				}
			}
		}
	}
	
	public Map<String, Field> getFieldMap() {
		return fieldMap;
	}

	public Collection<Field> getChildren() {
		return fieldMap.values();
	}

	public Field getChild(String name) {
		return fieldMap.get(name);
	}

	public Column getColumn(String column) {
		return allColumnMap.get(column);
	}

	public Map<String, Relation> getRelationMap() {
		return relationMap;
	}

	public Map<String, Column> getIdColumnMap() {
		return idColumnMap;
	}

	public Map<String, Column> getColumnMap() {
		return columnMap;
	}

	public Map<String, Column> getAllColumnMap() {
		return allColumnMap;
	}
	
	public Field findField(Class<? extends Field> type, String key, String value) {
		return findField(this, type, key, value);
	}
	
	public Field findField(Node node, Class<? extends Field> type, String key, String value) {
		Field result = null;
		if(node.getClass().getName().equals(type.getName())){
			try {
				if(PropertyUtils.getProperty(node, key).toString().equals(value)){
					result = (Field)node;
				}	
			} catch (Exception ignored) {}
		}else{
			if (node.getChildren() != null) {
                for (Node child : node.getChildren()) {
                    result = findField(child, type, key, value);
                    if (result != null) {
                        break;
                    }
                }
			}	
		}
		return result;
	}

	public Column findColumn(String column) {
		return findColumn(this, column);
	}

	public Column findColumn(Node node, String column) {
		Column result = null;
		if (node.getChildren() != null) {
            for (Node child : node.getChildren()) {
                result = findColumn(child, column);
                if (result != null) {
                    break;
                }
            }
		} else {
			if (node instanceof Column) {
				Column col = (Column) node;
				if (column.equals(col.getColumn())) {
					result = col;
				}
			}
		}
		return result;
	}

	public void prepareColumnMap(){
		idColumnMap = new TreeMap<String, Column>();
		columnMap = new TreeMap<String, Column>();
		allColumnMap = new TreeMap<String, Column>();
		prepareColumnMap(this);
	}

	private void prepareColumnMap(Node node) {
		if (node.getChildren() != null) {
            for (Node child : node.getChildren()) {
                prepareColumnMap(child);
            }
		} else {
			if (node instanceof Column) {
				if (node instanceof Id) {
					Column column = (Column) node;
					idColumnMap.put(column.getColumn(), column);
					allColumnMap.put(column.getColumn(), column);
					column.setColumnAlias(getColumnAlias());
				} else {
					if (node instanceof Property || node.getParent() instanceof ManyToOne) {
						boolean parentisId = false;
						Node parent = node.getParent();
						while (parent != null) {
							if (parent instanceof Id) {
								parentisId = true;
								break;
							}
							parent = parent.getParent();
						}
						Column column = (Column) node;
						if (parentisId) {
							idColumnMap.put(column.getColumn(), column);
						} else {
							columnMap.put(column.getColumn(), column);
						}
						allColumnMap.put(column.getColumn(), column);
						column.setColumnAlias(getColumnAlias());
					}
				}
			}
		}
	}
	
	private static int counter = 0;
	private static synchronized String getColumnAlias() {
		return "caffein_column" + counter++;
	}
	
	public Object toObject(CaffeinConfig caffeinConfig, ResultSet resultSet) throws Exception {
		Object object = getProxyClass(caffeinConfig).newInstance();
		Object objectId = getId().toObject(caffeinConfig, null, resultSet);
		PropertyUtils.setProperty(object, getId().getName(), objectId);
		if(nonIdFieldsMap != null){
			for (Field field : nonIdFieldsMap.values()) {
				PropertyUtils.setProperty(object, field.getName(), field.toObject(caffeinConfig, object, resultSet));
			}			
		}
		((ProxyObject) object).setHandler(new ClazzContainerHandler(caffeinConfig, this, objectId));
		return object;
	}
	
	public void toObject(CaffeinConfig caffeinConfig, ResultSet resultSet, Object object) throws Exception {
		for (Field field : getChildren()) {
			PropertyUtils.setProperty(object, field.getName(), field.toObject(caffeinConfig, object, resultSet));
		}
	}
	
	public Object toEmptyObject(CaffeinConfig caffeinConfig) throws Exception {
		Object object = getRealClass(caffeinConfig).newInstance();
		for (Field field : getChildren()) {
			Object value = field.toEmptyObject(caffeinConfig, this);
			if(value != null){
				PropertyUtils.setSimpleProperty(object, field.getName(), value);	
			}
		}
		return object;
	}
	
	public Object copy(CaffeinConfig caffeinConfig, Object object) throws Exception {
		Object newObject = getProxyClass(caffeinConfig).newInstance();
		Object newObjectId = PropertyUtils.getProperty(object, getId().getName());
		PropertyUtils.setProperty(newObject, getId().getName(), newObjectId);
		if(nonIdFieldsMap != null){
			for (Field field : nonIdFieldsMap.values()) {
				PropertyUtils.setProperty(newObject, field.getName(), field.copy(caffeinConfig, PropertyUtils.getProperty(object, field.getName())));
			}
		}
		((ProxyObject) newObject).setHandler(new ClazzContainerHandler(caffeinConfig, this, newObjectId));
		return newObject;
	}
	
	public void validate(CaffeinConfig caffeinConfig, Object object, ValidateFor validateFor) throws Exception {
        for (Field field : getChildren()) {
            field.validate(caffeinConfig, this, object, validateFor);
        }
	}
	
	public Column pathToColumn(CaffeinConfig caffeinConfig, String path) throws Exception{
		if( allPathColumnMap == null ){
			allPathColumnMap = new TreeMap<String, Column>();
            for (String columnName : allColumnMap.keySet()) {
                Column column = allColumnMap.get(columnName);
                allPathColumnMap.put(column.getPath(caffeinConfig), column);
            }
		}
		return allPathColumnMap.get(path);
	}
	
	public void healthCheck(CaffeinConfig caffeinConfig) throws Exception{
		prepareColumnMap();
		//load object
		Object object = new GenericQuery(caffeinConfig, this, "health_check").uniqueResult();
		if(object == null){
			//table is empty create empty object
			object = toEmptyObject(caffeinConfig);
		}
		for(Column column : allColumnMap.values()){
			//check column and table
			caffeinConfig.getColumnType(getTable(), column.getColumn());
			//check path
			String path = column.getPath(caffeinConfig);
			try {
				PropertyUtils.getProperty(object, path);	
			} catch (Exception e) {
				throw new ClazzConfigException("path not found for the clazz:"+name+" path:"+path, e, column);
			}
		}
	}

	public String toString() {
		return getClass().getName()+"["+name+"]";
	}
}